/** MIDI keyboard
 *
 * @author pquiring
 *
 * Created : Feb 17, 2014
 */

import java.awt.*;
import java.util.*;
import javax.sound.midi.*;

import javaforce.*;
import javaforce.media.*;

public class MidiKeyboard extends javax.swing.JDialog implements Receiver {

  /**
   * Creates new form MidiKeyboard
   */
  public MidiKeyboard(java.awt.Frame parent, boolean modal, Music music) {
    super(parent, modal);
    initComponents();
    buildTables(54.0f);
    if (!listDevices()) return;
    if (music != null) {
      this.music = music;
    } else {
      this.music = new Music();
      this.music.start(20, 40);  //20ms buffers, 40 channels max
    }
    this.music.soundClear();
    Library.load();
    loadLibrary();
    setPosition();
    JFImage icon = new JFImage();
    icon.loadPNG(this.getClass().getClassLoader().getResourceAsStream("jfmusic.png"));
    setIconImage(icon.getImage());
  }

  /**
   * This method is called from within the constructor to initialize the form. WARNING: Do NOT modify this code. The content of this method is always regenerated by the Form
   * Editor.
   */
  @SuppressWarnings("unchecked")
  // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
  private void initComponents() {

    jLabel1 = new javax.swing.JLabel();
    device = new javax.swing.JComboBox();
    jLabel2 = new javax.swing.JLabel();
    library = new javax.swing.JComboBox();
    close = new javax.swing.JButton();
    velocity = new javax.swing.JCheckBox();
    jLabel3 = new javax.swing.JLabel();
    region = new javax.swing.JComboBox();
    autoRegion = new javax.swing.JCheckBox();
    attenuation = new javax.swing.JSlider();
    jLabel4 = new javax.swing.JLabel();

    setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
    setTitle("Midi Keyboard");
    addWindowListener(new java.awt.event.WindowAdapter() {
      public void windowClosing(java.awt.event.WindowEvent evt) {
        formWindowClosing(evt);
      }
    });

    jLabel1.setText("Device:");

    device.addItemListener(new java.awt.event.ItemListener() {
      public void itemStateChanged(java.awt.event.ItemEvent evt) {
        deviceItemStateChanged(evt);
      }
    });

    jLabel2.setText("Instrument:");

    library.addItemListener(new java.awt.event.ItemListener() {
      public void itemStateChanged(java.awt.event.ItemEvent evt) {
        libraryItemStateChanged(evt);
      }
    });

    close.setText("Close");
    close.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        closeActionPerformed(evt);
      }
    });

    velocity.setSelected(true);
    velocity.setText("Velocity");

    jLabel3.setText("Region:");

    autoRegion.setSelected(true);
    autoRegion.setText("Auto");

    attenuation.setMajorTickSpacing(25);
    attenuation.setMinorTickSpacing(5);
    attenuation.setPaintTicks(true);
    attenuation.setToolTipText("");
    attenuation.addChangeListener(new javax.swing.event.ChangeListener() {
      public void stateChanged(javax.swing.event.ChangeEvent evt) {
        attenuationStateChanged(evt);
      }
    });

    jLabel4.setText("Attenuation");

    javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
    getContentPane().setLayout(layout);
    layout.setHorizontalGroup(
      layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(layout.createSequentialGroup()
        .addContainerGap()
        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
          .addGroup(layout.createSequentialGroup()
            .addComponent(jLabel1)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(device, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
          .addGroup(layout.createSequentialGroup()
            .addComponent(jLabel2)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(library, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
          .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
            .addComponent(velocity)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
            .addComponent(close))
          .addGroup(layout.createSequentialGroup()
            .addComponent(jLabel3)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(autoRegion)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(region, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
          .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
            .addComponent(jLabel4)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(attenuation, javax.swing.GroupLayout.PREFERRED_SIZE, 319, javax.swing.GroupLayout.PREFERRED_SIZE)))
        .addContainerGap())
    );
    layout.setVerticalGroup(
      layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(layout.createSequentialGroup()
        .addContainerGap()
        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
          .addComponent(jLabel1)
          .addComponent(device, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
          .addComponent(jLabel2)
          .addComponent(library, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
          .addComponent(jLabel3)
          .addComponent(region, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
          .addComponent(autoRegion))
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
          .addComponent(attenuation, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
          .addComponent(jLabel4, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
          .addComponent(close)
          .addComponent(velocity))
        .addContainerGap())
    );

    pack();
  }// </editor-fold>//GEN-END:initComponents

  private void libraryItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_libraryItemStateChanged
    loadRegions();
    loadSound();
  }//GEN-LAST:event_libraryItemStateChanged

  private void deviceItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_deviceItemStateChanged
    loadMIDI();
  }//GEN-LAST:event_deviceItemStateChanged

  private void closeActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_closeActionPerformed
    if (exit) System.exit(0); else dispose();
  }//GEN-LAST:event_closeActionPerformed

  private void formWindowClosing(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowClosing
    if (exit) System.exit(0); else dispose();
  }//GEN-LAST:event_formWindowClosing

  private void attenuationStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_attenuationStateChanged
    float att = getAttenuation();
    JFLog.log("attenuation = " + att);
    for(int a=0;a<sounds.length;a++) {
      music.soundAttenuation(sounds[a], att);
    }
  }//GEN-LAST:event_attenuationStateChanged

  /** Runs MIDI Keyboard stand alone
   *
   * @param args the command line arguments
   */
  public static void main(String args[]) {
    /* Create and display the dialog */
    java.awt.EventQueue.invokeLater(new Runnable() {
      public void run() {
        MidiKeyboard dialog = new MidiKeyboard(null, true, null);
        dialog.exit = true;
        dialog.setVisible(true);
      }
    });
  }

  // Variables declaration - do not modify//GEN-BEGIN:variables
  private javax.swing.JSlider attenuation;
  private javax.swing.JCheckBox autoRegion;
  private javax.swing.JButton close;
  private javax.swing.JComboBox device;
  private javax.swing.JLabel jLabel1;
  private javax.swing.JLabel jLabel2;
  private javax.swing.JLabel jLabel3;
  private javax.swing.JLabel jLabel4;
  private javax.swing.JComboBox library;
  private javax.swing.JComboBox region;
  private javax.swing.JCheckBox velocity;
  // End of variables declaration//GEN-END:variables

  private Music music;
  private ArrayList<MidiDevice> list = new ArrayList<MidiDevice>();
  private MidiDevice midi;
  private Transmitter trans;
  private int idxes[] = new int[0x80];  //play back channel indexes
  private float vols[] = new float[0x80];
  private boolean exit = false;
  private int regions[][];
  private int sounds[];  //loaded sound index

  private boolean listDevices() {
    device.removeAllItems();
    device.addItem("Select device");
    list.clear();
    MidiDevice.Info[] infos = MidiSystem.getMidiDeviceInfo();
    try {
      if (infos == null) throw new Exception("no devices found");
      for(int a=0;a<infos.length;a++) {
        MidiDevice midiDevice = MidiSystem.getMidiDevice(infos[a]);
        if (midiDevice.getMaxTransmitters() == 0) continue;
        list.add(midiDevice);
        device.addItem(infos[a].getName());
      }
      if (device.getItemCount() == 0) throw new Exception("no devices found");
      return true;
    } catch (Exception e) {
      JFLog.log(e);
      JFAWT.showError("Error", "No devices found");
      if (exit) System.exit(0); else dispose();
      return false;
    }
  }

  private void loadLibrary() {
    library.removeAllItems();
    ArrayList<Library.Entry> list = Library.getList();
    for(int a=0;a<list.size();a++) {
      library.addItem(list.get(a).name);
    }
  }

  private void loadRegions() {
    int idx = library.getSelectedIndex();
    if (idx == -1) return;
    Library.Entry entry = Library.get(idx);
    region.removeAllItems();
    switch (entry.type) {
      case WAV:
        //no regions
        regions = new int[1][3];
        regions[0][0] = 0;
        regions[0][1] = 0x7f;
        regions[0][2] = 0x3c;  //middle C (60)
        region.addItem("Full Range (Middle C)");
        break;
      case DLS:
        DLS dls = Library.getDLS(entry.dls_idx);
        int cnt = dls.getRegionsCount(entry.name);
        regions = new int[cnt][3];
        for(int a=0;a<cnt;a++) {
          DLS.Region r = dls.getRegion(entry.name, a);
          regions[a][0] = r.keyMin;
          regions[a][1] = r.keyMax;
          regions[a][2] = r.unityNote;
          region.addItem("Keys " + DLS.getKeyName(r.keyMin) + " thru " + DLS.getKeyName(r.keyMax));
        }
        break;
    }
  }

  private float getAttenuation() {
    return attenuation.getValue() / 100.f * 0.00005f;
  }

  private void loadSound() {
    music.soundClear();
    int idx = library.getSelectedIndex();
    if (idx == -1) return;
    Library.Entry entry = Library.get(idx);
    String name = entry.name;
    switch (entry.type) {
      case WAV:
        sounds[0] = music.soundLoad("library/" + entry.name, -1, -1, -1, -1, getAttenuation());
        break;
      case DLS:
        DLS dls = Library.getDLS(entry.dls_idx);
        int cnt = dls.getRegionsCount(name);
        sounds = new int[cnt];
        for(int r=0;r<cnt;r++) {
          DLS.Instrument i = dls.getInstrument(name, r);
          DLS.Region re = dls.getRegion(name, r);
          if (i == null) continue;
          int susStart = -1, susEnd = -1;
          if (i.loopStart != -1) {
            susStart = i.loopStart;
            susEnd = i.loopStart + i.loopLength;
          }
          sounds[r] = music.soundLoad(i.samples, -1, -1, susStart, susEnd, getAttenuation(), re.unityNote);
        }
        break;
    }
  }

  private void loadMIDI() {
    if (midi != null) {
      midi.close();
      midi = null;
    }
    int idx = device.getSelectedIndex();
    if (idx == -1) return;
    if (idx == 0) return;
    idx--;
    midi = list.get(idx);
    try {
      trans = midi.getTransmitter();
      trans.setReceiver(this);
      midi.open();
      JFLog.log("selected device:" + midi + "," + trans);
    } catch (Exception e) {
      JFLog.log(e);
      midi.close();
      JFAWT.showError("Error", "Failed to open device");
    }
  }

  //C-2=0x00 C-1=0x0c C0=0x18 C1=0x24 C2=0x30 ... C8=0x78 ... C9=0x7f
  //C5 = 1.0f

  private void buildTables(float baseFreq) {
//    JFLog.log("buildTables : baseFreq=" + baseFreq);
    for(int a=0;a<=0x7f;a++) {
      idxes[a] = -1;
      vols[a] = ((float)a) / 127.0f;
    }
  }

  private void setPosition() {
    Dimension d = getSize();
    Rectangle s = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
    setLocation(s.width/2 - d.width/2, s.height/2 - d.height/2);
  }

  private int getRegionOfNote(int note) {
    for(int r=0;r<regions.length;r++) {
      if (note >= regions[r][0] && note <= regions[r][1]) return r;
    }
    return -1;
  }

  //Receiver.close() - do nothing
  public void close() {}

  //Receiver.send() - receive MIDI message
  public void send(MidiMessage message, long timeStamp) {
    if (!(message instanceof ShortMessage)) return;
    //decode message
    ShortMessage sm = (ShortMessage)message;
    int cmd = sm.getCommand() & 0xff;
    int d1 = sm.getData1() & 0x7f;
    int d2 = sm.getData2() & 0x7f;
    String cmdstr = null;

    switch (cmd) {
      case 0x80:  //note off
        // note / velocity (zero)
        cmdstr = "keyUp";
        if (idxes[d1] != -1) {
          if (false) {
            music.channelKeyUp(idxes[d1]);  //sounds abrupt
          } else {
            music.channelAttenuation(idxes[d1], 0.0001f);  //increase attenutation greatly
          }
          idxes[d1] = -1;
        }
        break;
      case 0x90:  //note on
        // note / velocity (0-127)
        cmdstr = "keyDn";
        if (!velocity.isSelected()) d2 = 0x7f;
        int regionIdx;
        if (autoRegion.isSelected())
          regionIdx = getRegionOfNote(d1);
        else
          regionIdx = region.getSelectedIndex();
        if (regionIdx == -1) break;
        JFLog.log("region=" + regionIdx);
        idxes[d1] = music.soundPlay(sounds[regionIdx], vols[d2], vols[d2], d1);
        //remove index from other notes (in case user has still not released note)
        for(int a=0;a<0x80;a++) {
          if (a == d1) continue;
          if (idxes[a] == idxes[d1]) {
            idxes[a] = -1;
          }
        }
        break;
      case 0xb0:  //control change
        cmdstr = " ctrl";
        // control / value (0-127)
        // controls : 1 = modulation : 7 = volume : 4a = ch1 : 47 = ch2 : 49 = ch3 : 48 = ch4
        break;
      case 0xe0:  //pitch wheel
        cmdstr = "pitch";
        // zero / value (0-127)
        break;
    }
    JFLog.log(String.format("%s:%02x:%02x:%02x", cmdstr, cmd, d1, d2));
  }
}
